<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hyena Diva Studio v2.1 ⚡️</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<style>
:root {
--bg-gradient: radial-gradient(circle at 20% 20%, #fff5ff 0%, #ffe3ff 35%, #f9f8ff 75%, #f4f7ff 100%);
--accent: #7d00b9;
--accent-light: rgba(125, 0, 185, 0.1);
--text: #2d1038;
--panel-bg: rgba(255, 255, 255, 0.95);
--border: rgba(45, 16, 56, 0.15);
}
* { box-sizing: border-box; }
body {
margin: 0;
height: 100vh;
overflow: hidden;
font-family: "Trebuchet MS", system-ui, sans-serif;
background: var(--bg-gradient);
color: var(--text);
display: flex;
flex-direction: column;
user-select: none;
}
/* Loading */
#loading-overlay {
position: fixed; inset: 0; background: rgba(255,255,255,0.98); z-index: 9999;
display: flex; flex-direction: column; align-items: center; justify-content: center;
transition: opacity 0.5s ease;
}
#loading-overlay.hidden { opacity: 0; pointer-events: none; }
.spinner {
width: 40px; height: 40px; border: 4px solid #ddd; border-top-color: var(--accent);
border-radius: 50%; animation: load-spin 1s linear infinite; margin-bottom: 1rem;
}
@keyframes load-spin { to { transform: rotate(360deg); } }
/* --- 3-PANE LAYOUT --- */
#app-layout {
display: grid;
grid-template-columns: 320px 1fr 300px;
grid-template-rows: 1fr;
height: 100vh;
gap: 1px;
background: var(--border);
}
.panel-col {
background: var(--panel-bg);
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.panel-content {
padding: 1rem;
overflow-y: auto;
flex: 1;
display: flex;
flex-direction: column;
gap: 1rem;
}
/* LEFT: Library */
.library-header {
padding: 1rem 1rem 0.5rem 1rem;
background: var(--panel-bg);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.prop-search {
width: 100%; padding: 0.6rem; border-radius: 8px; border: 1px solid var(--border);
margin-bottom: 0.5rem; font-family: inherit; font-size: 0.9rem;
}
.category-tabs {
display: flex; flex-wrap: wrap; gap: 0.25rem;
}
.category-tabs button {
font-size: 0.7rem; padding: 0.3rem 0.6rem; border-radius: 12px;
border: 1px solid var(--border); background: #fff; cursor: pointer;
text-transform: capitalize;
}
.category-tabs button:hover { background: #eee; }
.category-tabs button.is-active {
background: var(--accent); color: white; border-color: var(--accent);
}
.tile-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 0.5rem;
}
.tile {
border: 1px solid transparent; border-radius: 8px; padding: 0.25rem;
background: #fff; cursor: pointer; text-align: center;
transition: all 0.1s;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.tile:hover { transform: translateY(-2px); border-color: var(--accent); }
.tile img { width: 100%; height: 70px; object-fit: contain; display: block; image-rendering: pixelated; }
.tile span { display: block; font-size: 0.65rem; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; opacity: 0.7; }
/* CENTER: Stage */
.stage-area {
background: #e0e0e5;
display: flex; flex-direction: column; align-items: center; justify-content: center;
position: relative; overflow: hidden;
}
.stage-wrapper {
position: relative;
box-shadow: 0 20px 50px rgba(0,0,0,0.2);
border-radius: 4px;
overflow: hidden;
border: 4px solid #fff;
transition: width 0.3s ease, height 0.3s ease;
}
#stage {
width: 100%; height: 100%;
background: #333 center/cover no-repeat;
position: relative;
touch-action: none;
}
/* Independent Grid Overlay */
#grid-overlay {
position: absolute; inset: 0; pointer-events: none; z-index: 9999;
background: linear-gradient(rgba(125,0,185,0.2) 1px, transparent 1px), linear-gradient(90deg, rgba(125,0,185,0.2) 1px, transparent 1px);
background-size: 50px 50px;
display: none;
}
/* Stage Toolbar */
.stage-toolbar {
position: absolute; bottom: 20px;
background: rgba(255,255,255,0.95); backdrop-filter: blur(4px);
padding: 0.4rem; border-radius: 50px; border: 1px solid var(--border);
display: flex; gap: 0.4rem; align-items: center;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 100;
}
.dim-select {
border: 1px solid var(--border); border-radius: 20px; padding: 0.3rem 0.6rem;
font-size: 0.75rem; background: #fff; color: var(--text); cursor: pointer; outline: none;
}
.icon-btn {
width: 38px; height: 38px; border-radius: 50%; border: 1px solid var(--border);
background: #fff; display: flex; align-items: center; justify-content: center;
cursor: pointer; font-size: 1.2rem; transition: transform 0.1s;
position: relative;
}
.icon-btn:hover { background: var(--accent-light); color: var(--accent); transform: scale(1.05); }
.icon-btn:active { transform: scale(0.95); }
/* Tooltip */
.icon-btn::after {
content: attr(title); position: absolute; bottom: 110%; left: 50%; transform: translateX(-50%);
background: #2d1038; color: #fff; font-size: 0.7rem; padding: 4px 8px; border-radius: 4px;
opacity: 0; pointer-events: none; transition: opacity 0.2s; white-space: nowrap;
}
.icon-btn:hover::after { opacity: 1; }
/* RIGHT: Inspector */
.inspector-header {
font-size: 0.8rem; font-weight: bold; text-transform: uppercase; letter-spacing: 0.05em;
color: var(--accent); border-bottom: 2px solid var(--accent-light);
padding-bottom: 0.5rem; margin-bottom: 0.5rem;
}
.inspector-group {
background: #fff; border: 1px solid var(--border); border-radius: 8px;
padding: 0.75rem; display: flex; flex-direction: column; gap: 0.75rem;
}
.control-row {
display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.7rem; color: #555;
font-weight: 600; text-transform: uppercase;
}
.compact-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem;
}
input[type=range] {
width: 100%; height: 4px; border-radius: 2px; -webkit-appearance: none; background: #ddd;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%;
background: var(--accent); cursor: pointer; border: 2px solid #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.nudge-grid {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px;
width: 100px; margin: 0 auto;
}
.nudge-btn {
padding: 4px; font-size: 1rem; border: 1px solid var(--border);
background: #f8f8f8; border-radius: 4px; cursor: pointer;
}
.nudge-btn:hover { background: var(--accent-light); color: var(--accent); }
.action-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem;
}
.action-btn {
padding: 8px; font-size: 0.7rem; background: var(--accent-light);
border: 1px solid transparent; border-radius: 6px; color: var(--accent); font-weight: 700;
cursor: pointer; text-transform: uppercase;
}
.action-btn:hover { background: var(--accent); color: white; }
.action-btn.danger { background: #ffeebb; color: #a33; }
.action-btn.danger:hover { background: #d00; color: white; }
.action-btn.reset { background: #eee; color: #666; }
.action-btn.reset:hover { background: #ccc; color: #333; }
/* Stage Items */
.stage-item {
position: absolute;
transform-origin: center;
cursor: grab;
touch-action: none;
user-select: none;
}
.stage-item img {
display: block; width: 100%; height: auto;
pointer-events: none;
image-rendering: pixelated;
filter: var(--f, none); opacity: var(--o, 1);
}
.stage-item.is-selected {
outline: 2px dashed rgba(255,255,255,0.8);
box-shadow: 0 0 0 2px var(--accent);
z-index: 1000 !important;
animation: select-pulse 2s infinite; /* Polished active state */
}
.stage-item:active { cursor: grabbing; }
@keyframes select-pulse {
0%, 100% { box-shadow: 0 0 0 2px var(--accent); }
50% { box-shadow: 0 0 8px 3px var(--accent); }
}
/* Animation Classes */
.anim-float { animation: floaty var(--spd, 2s) ease-in-out infinite; }
.anim-bob { animation: bob var(--spd, 1s) ease-in-out infinite alternate; }
.anim-spin { animation: spin var(--spd, 4s) linear infinite; }
.anim-pulse { animation: pulse var(--spd, 1s) ease-in-out infinite; }
.anim-shimmer { animation: shimmer var(--spd, 2s) ease-in-out infinite; }
@keyframes floaty { 0%,100% { transform: translate(-50%, -50%) translateY(0) rotate(var(--r)) scale(var(--sx), var(--sy)); } 50% { transform: translate(-50%, -50%) translateY(-15px) rotate(var(--r)) scale(var(--sx), var(--sy)); } }
@keyframes bob { 0% { transform: translate(-50%, -50%) translateY(0) rotate(var(--r)) scale(var(--sx), var(--sy)); } 100% { transform: translate(-50%, -50%) translateY(8px) rotate(var(--r)) scale(var(--sx), var(--sy)); } }
@keyframes spin { from { transform: translate(-50%, -50%) rotate(0deg) scale(var(--sx), var(--sy)); } to { transform: translate(-50%, -50%) rotate(360deg) scale(var(--sx), var(--sy)); } }
@keyframes pulse { 0%, 100% { opacity: var(--o); } 50% { opacity: calc(var(--o) * 0.5); } }
@keyframes shimmer { 0%,100% { filter: var(--f) drop-shadow(0 0 0 rgba(255,255,255,0)); } 50% { filter: var(--f) drop-shadow(0 0 10px rgba(255,255,255,0.8)); } }
@media (max-width: 900px) {
#app-layout {
grid-template-columns: 1fr; grid-template-rows: 250px 400px 1fr;
height: auto; overflow-y: auto;
}
.stage-wrapper { transform: scale(0.6); transform-origin: top center; margin-bottom: -200px; }
.panel-content { overflow: visible; }
}
</style>
</head>
<body>
<div id="loading-overlay">
<div class="spinner"></div>
<div id="loading-text">Reading Synaptic Folders...</div>
</div>
<div id="app-layout">
<aside class="panel-col">
<div class="library-header">
<h3 style="margin:0 0 0.5rem 0;">Assets ⚡️</h3>
<input type="search" id="prop-search" class="prop-search" placeholder="Search files..." autocomplete="off" />
<div class="category-tabs" id="category-tabs"></div>
</div>
<div class="panel-content">
<div id="library-status" style="font-size:0.8rem; opacity:0.6; margin-bottom:0.5rem;"></div>
<div class="tile-grid" id="asset-list"></div>
<button id="load-more" style="width:100%; margin-top:1rem; padding:0.5rem; display:none; border:none; background:#eee; cursor:pointer; border-radius:4px;">Load More</button>
</div>
</aside>
<main class="stage-area">
<div class="stage-wrapper" id="stage-wrapper" style="width: 800px; height: 600px;">
<div id="stage">
<div id="grid-overlay"></div>
</div>
</div>
<div class="stage-toolbar">
<select class="dim-select" id="stage-dim" title="Stage Dimensions">
<option value="800x600">Classic (4:3)</option>
<option value="1024x576">Widescreen (16:9)</option>
<option value="500x800">Vertical (Mobile)</option>
</select>
<div style="width:1px; background:#ccc; margin:0 4px; height: 20px;"></div>
<button class="icon-btn" id="clear-stage" title="Clear Stage">🗑️</button>
<button class="icon-btn" id="grid-toggle" title="Toggle Grid">▦</button>
<div style="width:1px; background:#ccc; margin:0 4px; height: 20px;"></div>
<button class="icon-btn" id="copy-scene" title="Copy Scene to Clipboard">📋</button>
<button class="icon-btn" id="paste-scene" title="Paste Scene from Clipboard">📥</button>
<div style="width:1px; background:#ccc; margin:0 4px; height: 20px;"></div>
<button class="icon-btn" id="save-scene" title="Save JSON File">💾</button>
<button class="icon-btn" id="load-scene" title="Load JSON File">📂</button>
<button class="icon-btn" id="export-img" title="Export as PNG Screenshot">📸</button>
<input type="file" id="scene-input" hidden accept=".json">
</div>
</main>
<aside class="panel-col">
<div class="panel-content">
<div class="inspector-header">Inspector</div>
<div id="inspector-empty" style="text-align:center; padding:2rem; opacity:0.6; font-size:0.9rem;">
Select a localized pattern to edit.
</div>
<div id="inspector-controls" hidden>
<div style="font-weight:bold; margin-bottom:0.5rem; font-size:0.85rem; word-break:break-all;" id="selected-name">Item Name</div>
<div class="inspector-group">
<div class="compact-grid">
<div class="control-row">
<label>Size</label>
<input type="range" id="size-range" min="10" max="300" value="100">
</div>
<div class="control-row">
<label>Rotation</label>
<input type="range" id="rot-range" min="-180" max="180" value="0">
</div>
</div>
<div class="action-grid">
<button class="action-btn" id="btn-flip">Flip H</button>
<button class="action-btn" id="btn-center">Center</button>
</div>
<div class="nudge-grid" title="Use Arrow Keys on Keyboard to Nudge!">
<div></div><button class="nudge-btn" id="n-up">↑</button><div></div>
<button class="nudge-btn" id="n-left">←</button><div style="font-size:10px;text-align:center;display:flex;align-items:center;justify-content:center;">MOVE</div><button class="nudge-btn" id="n-right">→</button>
<div></div><button class="nudge-btn" id="n-down">↓</button><div></div>
</div>
</div>
<div class="inspector-group">
<div class="compact-grid">
<div class="control-row">
<label>Opacity</label>
<input type="range" id="op-range" min="0" max="100" value="100">
</div>
<div class="control-row">
<label>Blur</label>
<input type="range" id="blur-range" min="0" max="10" value="0">
</div>
<div class="control-row">
<label>Brightness</label>
<input type="range" id="bri-range" min="0" max="200" value="100">
</div>
<div class="control-row">
<label>Shadow</label>
<input type="range" id="shd-range" min="0" max="20" value="0">
</div>
<div class="control-row">
<label>Hue</label>
<input type="range" id="hue-range" min="-180" max="180" value="0">
</div>
<div class="control-row">
<label>Saturation</label>
<input type="range" id="sat-range" min="0" max="200" value="100">
</div>
</div>
<button class="action-btn reset" id="btn-reset-fx">Reset Effects</button>
</div>
<div class="inspector-group">
<div class="control-row">
<label>Animation</label>
<select id="anim-select" style="width:100%; padding:4px;">
<option value="none">None</option>
<option value="float">Float</option>
<option value="bob">Bob</option>
<option value="shimmer">Shimmer</option>
<option value="pulse">Pulse</option>
<option value="spin">Spin</option>
</select>
</div>
<div class="control-row">
<label>Speed</label>
<input type="range" id="anim-speed" min="1" max="50" value="20">
</div>
</div>
<div class="inspector-group">
<div class="action-grid">
<button class="action-btn" id="btn-front">To Front</button>
<button class="action-btn" id="btn-back">To Back</button>
<button class="action-btn" id="btn-clone">Duplicate</button>
<button class="action-btn danger" id="btn-del">Delete</button>
</div>
</div>
</div>
</div>
</aside>
</div>
<script>
/* =========================================
CONFIG
========================================= */
const GITHUB = { owner: "TheNabu222", repo: "entropic-ai", branch: "main" };
const RAW_URL = `https://raw.githubusercontent.com/${GITHUB.owner}/${GITHUB.repo}/${GITHUB.branch}`;
const API_URL = `https://api.github.com/repos/${GITHUB.owner}/${GITHUB.repo}/git/trees/${GITHUB.branch}?recursive=1`;
const IGNORE_FOLDERS = ["_archive", "old", "wip", "debug", "navbar", "ui", "icons", "website", "_hdtv", "favicons", "logo"];
// State
let allAssets = [];
let filteredAssets = [];
let renderedCount = 0;
let activeCategory = "backdrops";
let selectedEl = null;
let zIndexCounter = 100;
const dom = {
stageWrapper: document.getElementById('stage-wrapper'),
stage: document.getElementById('stage'),
gridOverlay: document.getElementById('grid-overlay'),
assetList: document.getElementById('asset-list'),
tabs: document.getElementById('category-tabs'),
search: document.getElementById('prop-search'),
loadMore: document.getElementById('load-more'),
inspectorEmpty: document.getElementById('inspector-empty'),
inspectorControls: document.getElementById('inspector-controls'),
selectedName: document.getElementById('selected-name'),
libStatus: document.getElementById('library-status')
};
/* =========================================
DATA LOGIC
========================================= */
async function init() {
try {
const res = await fetch(API_URL);
if (!res.ok) throw new Error("GitHub Limit");
const data = await res.json();
processAssetsByFolder(data.tree);
renderUI();
document.getElementById('loading-overlay').classList.add('hidden');
} catch (e) {
alert("Error loading assets from GitHub. You may have hit the hourly API limit. Try again shortly.");
console.error(e);
document.getElementById('loading-overlay').classList.add('hidden');
}
}
function processAssetsByFolder(tree) {
allAssets = tree
.filter(file => file.path.match(/\.(png|jpg|jpeg|gif|webp)$/i))
.filter(file => !IGNORE_FOLDERS.some(term => file.path.toLowerCase().includes(term)))
.map(file => {
const parts = file.path.split('/');
const filename = parts.pop();
let folderPath = parts.join('/');
folderPath = folderPath.replace(/^cavebot\/assets\//, "");
folderPath = folderPath.replace(/^assets\//, "");
folderPath = folderPath.replace(/^image\//, "");
const cleanParts = folderPath.split('/');
let category = cleanParts[0] || "Misc";
category = category.replace(/[_-]/g, " ").toLowerCase();
let label = filename
.replace(/\.[^/.]+$/, "")
.replace(/[_-]/g, " ")
.replace(/cavebot/gi, "")
.replace(/^\d+/, "")
.trim();
return {
id: file.path,
url: `${RAW_URL}/${file.path}`,
label: label || filename,
category: category,
fullPath: file.path
};
});
}
/* =========================================
UI RENDERING
========================================= */
function renderUI() {
const cats = [...new Set(allAssets.map(a => a.category))].sort();
dom.tabs.innerHTML = cats.map(c =>
`<button onclick="setCategory('${c}')" class="${c === activeCategory ? 'is-active' : ''}">${c}</button>`
).join('');
if (!cats.includes(activeCategory)) {
activeCategory = cats.includes('backdrops') ? 'backdrops' : cats[0];
}
updateTabs();
filterAssets();
}
function updateTabs() {
const btns = dom.tabs.querySelectorAll('button');
btns.forEach(b => {
b.classList.toggle('is-active', b.innerText === activeCategory);
});
}
window.setCategory = (cat) => {
activeCategory = cat;
updateTabs();
filterAssets();
}
function filterAssets() {
const term = dom.search.value.toLowerCase();
filteredAssets = allAssets.filter(a => {
const matchCat = a.category === activeCategory;
const matchSearch = a.label.toLowerCase().includes(term);
return matchCat && matchSearch;
});
dom.libStatus.innerText = `${filteredAssets.length} items`;
renderedCount = 0;
dom.assetList.innerHTML = '';
renderNextBatch();
}
function renderNextBatch() {
const batch = filteredAssets.slice(renderedCount, renderedCount + 50);
if (batch.length === 0 && renderedCount === 0) {
dom.assetList.innerHTML = '<div style="grid-column:1/-1;text-align:center;opacity:0.5;font-size:0.8rem;">No items</div>';
dom.loadMore.style.display = 'none';
return;
}
const html = batch.map(item => `
<div class="tile" onclick="handleAssetClick('${item.id}')" title="${item.label}">
<img src="${item.url}" loading="lazy">
<span>${item.label}</span>
</div>
`).join('');
dom.assetList.insertAdjacentHTML('beforeend', html);
renderedCount += batch.length;
dom.loadMore.style.display = renderedCount < filteredAssets.length ? 'block' : 'none';
}
dom.search.addEventListener('input', () => filterAssets());
dom.loadMore.addEventListener('click', renderNextBatch);
/* =========================================
STAGE LOGIC
========================================= */
window.handleAssetClick = (id) => {
const asset = allAssets.find(a => a.id === id);
if (!asset) return;
if (asset.category.includes('backdrop')) {
dom.stage.style.backgroundImage = `url("${asset.url}")`;
dom.stage.dataset.bgId = id;
} else {
spawnProp(asset);
}
};
function spawnProp(asset, state = null) {
const el = document.createElement('div');
el.className = 'stage-item';
// BUG FIX: Ensure new items spawn on top of loaded items.
el.style.zIndex = state && state.z ? state.z : ++zIndexCounter;
// Initial Position
el.style.left = state ? state.x : '50%';
el.style.top = state ? state.y : '50%';
// State
el.dataset.id = asset.id;
el.dataset.s = state ? state.s : 100;
el.dataset.r = state ? state.r : 0;
el.dataset.flip = state ? state.flip : 1;
el.dataset.anim = state ? state.anim : 'none';
el.dataset.spd = state ? state.spd : 20;
// Visuals
el.dataset.op = state ? state.op : 100;
el.dataset.bl = state ? state.bl : 0;
el.dataset.hue = state ? state.hue : 0;
el.dataset.sat = state ? state.sat : 100;
el.dataset.bri = state ? state.bri : 100;
el.dataset.shd = state ? state.shd : 0;
const img = document.createElement('img');
img.src = asset.url;
img.draggable = false;
// BUG FIX: Prevent massive assets from taking over the screen
img.onload = () => {
let w = img.naturalWidth;
let h = img.naturalHeight;
const MAX_DIM = 400; // Cap initial visual size
if (w > MAX_DIM || h > MAX_DIM) {
const ratio = Math.min(MAX_DIM / w, MAX_DIM / h);
w = w * ratio;
h = h * ratio;
}
el.style.width = w + 'px';
el.style.height = h + 'px';
updateVisuals(el);
};
el.appendChild(img);
dom.stage.appendChild(el);
updateVisuals(el);
selectItem(el);
enableDrag(el);
}
function selectItem(el) {
if (selectedEl) selectedEl.classList.remove('is-selected');
selectedEl = el;
if (el) {
el.classList.add('is-selected');
dom.inspectorEmpty.hidden = true;
dom.inspectorControls.hidden = false;
const asset = allAssets.find(a => a.id === el.dataset.id);
dom.selectedName.innerText = asset ? asset.label : 'Unknown';
syncInspector();
} else {
dom.inspectorEmpty.hidden = false;
dom.inspectorControls.hidden = true;
}
}
// Deselect if clicking blank stage
dom.stage.addEventListener('pointerdown', (e) => {
if(e.target === dom.stage || e.target === dom.gridOverlay) selectItem(null);
});
function enableDrag(el) {
let isDragging = false;
let startX, startY, initLeft, initTop;
el.addEventListener('pointerdown', (e) => {
e.preventDefault();
e.stopPropagation();
selectItem(el);
isDragging = true;
el.setPointerCapture(e.pointerId);
startX = e.clientX;
startY = e.clientY;
initLeft = parseFloat(el.style.left) || el.offsetLeft;
initTop = parseFloat(el.style.top) || el.offsetTop;
});
el.addEventListener('pointermove', (e) => {
if (!isDragging) return;
// Get bounding rect to account for responsive scaling
const rect = dom.stage.getBoundingClientRect();
const scaleX = dom.stage.offsetWidth / rect.width;
const scaleY = dom.stage.offsetHeight / rect.height;
const dx = (e.clientX - startX) * scaleX;
const dy = (e.clientY - startY) * scaleY;
el.style.left = `${initLeft + dx}px`;
el.style.top = `${initTop + dy}px`;
});
el.addEventListener('pointerup', (e) => {
isDragging = false;
el.releasePointerCapture(e.pointerId);
});
}
/* =========================================
INSPECTOR
========================================= */
function updateVisuals(el = selectedEl) {
if (!el) return;
const s = (el.dataset.s || 100) / 100;
const r = el.dataset.r || 0;
const f = el.dataset.flip || 1;
el.style.setProperty('--r', `${r}deg`);
el.style.setProperty('--sx', f * s);
el.style.setProperty('--sy', s);
if (el.dataset.anim === 'none') {
el.style.transform = `translate(-50%, -50%) rotate(${r}deg) scale(${f * s}, ${s})`;
} else {
el.style.transform = '';
}
const bl = el.dataset.bl;
const hue = el.dataset.hue;
const sat = el.dataset.sat;
const bri = el.dataset.bri;
const shd = el.dataset.shd;
const filterStr = `blur(${bl}px) hue-rotate(${hue}deg) saturate(${sat}%) brightness(${bri}%)`;
const dropShadow = shd > 0 ? `drop-shadow(0 0 ${shd}px rgba(0,0,0,0.5))` : '';
const img = el.querySelector('img');
img.style.setProperty('--f', `${filterStr} ${dropShadow}`);
img.style.setProperty('--o', el.dataset.op / 100);
el.className = `stage-item is-selected anim-${el.dataset.anim}`;
el.style.setProperty('--spd', `${(51 - el.dataset.spd) / 10}s`);
}
function syncInspector() {
if (!selectedEl) return;
['s','r','op','bl','hue','sat','bri','shd','spd'].forEach(k => {
const input = document.querySelector(`input[id^="${k}"]`) || document.getElementById(`${k}-range`) || document.getElementById(`anim-speed`);
if(input) input.value = selectedEl.dataset[k];
});
document.getElementById('anim-select').value = selectedEl.dataset.anim;
}
const bindings = {
'size-range': 's', 'rot-range': 'r', 'op-range': 'op', 'blur-range': 'bl',
'hue-range': 'hue', 'sat-range': 'sat', 'bri-range': 'bri', 'shd-range': 'shd',
'anim-speed': 'spd'
};
Object.keys(bindings).forEach(id => {
document.getElementById(id).addEventListener('input', (e) => {
if(selectedEl) {
selectedEl.dataset[bindings[id]] = e.target.value;
updateVisuals();
}
});
});
document.getElementById('anim-select').addEventListener('change', (e) => {
if(selectedEl) { selectedEl.dataset.anim = e.target.value; updateVisuals(); }
});
document.getElementById('btn-flip').onclick = () => { if(selectedEl) { selectedEl.dataset.flip *= -1; updateVisuals(); } };
document.getElementById('btn-center').onclick = () => { if(selectedEl) { selectedEl.style.left = '50%'; selectedEl.style.top = '50%'; } };
document.getElementById('btn-reset-fx').onclick = () => {
if(selectedEl) {
Object.assign(selectedEl.dataset, { op:100, bl:0, hue:0, sat:100, bri:100, shd:0 });
syncInspector(); updateVisuals();
}
};
document.getElementById('btn-del').onclick = () => { if(selectedEl) { selectedEl.remove(); selectItem(null); } };
document.getElementById('btn-clone').onclick = () => {
if(selectedEl) {
const asset = allAssets.find(a => a.id === selectedEl.dataset.id);
if(asset) spawnProp(asset, {
...selectedEl.dataset,
x: parseFloat(selectedEl.style.left) + 20 + 'px',
y: parseFloat(selectedEl.style.top) + 20 + 'px',
z: ++zIndexCounter // BUG FIX: Ensure clones spawn on top
});
}
};
document.getElementById('btn-front').onclick = () => { if(selectedEl) selectedEl.style.zIndex = ++zIndexCounter; };
document.getElementById('btn-back').onclick = () => { if(selectedEl) selectedEl.style.zIndex = 0; };
// Nudge Logic
const nudge = (dx, dy) => {
if(!selectedEl) return;
selectedEl.style.left = (parseFloat(selectedEl.style.left) + dx) + 'px';
selectedEl.style.top = (parseFloat(selectedEl.style.top) + dy) + 'px';
};
document.getElementById('n-up').onclick = () => nudge(0, -10);
document.getElementById('n-down').onclick = () => nudge(0, 10);
document.getElementById('n-left').onclick = () => nudge(-10, 0);
document.getElementById('n-right').onclick = () => nudge(10, 0);
/* =========================================
TOOLBAR, SAVE/LOAD & EXPORT
========================================= */
// Resize Stage Toggle
document.getElementById('stage-dim').addEventListener('change', (e) => {
const [w, h] = e.target.value.split('x');
dom.stageWrapper.style.width = `${w}px`;
dom.stageWrapper.style.height = `${h}px`;
});
document.getElementById('clear-stage').onclick = () => {
Array.from(dom.stage.children).forEach(child => {
if(child.id !== 'grid-overlay') child.remove();
});
selectItem(null);
};
// BUG FIX: Grid abstracted to a separate layer so it doesn't taint JSON Saves
document.getElementById('grid-toggle').onclick = () => {
const grid = dom.gridOverlay;
grid.style.display = grid.style.display === 'block' ? 'none' : 'block';
};
// --- JSON Logic ---
function getSceneData() {
return {
bgId: dom.stage.dataset.bgId || null,
bgUrl: dom.stage.style.backgroundImage.replace(/url\(['"]?(.*?)['"]?\)/, '$1'),
items: Array.from(dom.stage.children)
.filter(el => el.id !== 'grid-overlay')
.map(el => ({
id: el.dataset.id,
x: el.style.left,
y: el.style.top,
z: el.style.zIndex,
...el.dataset
}))
};
}
document.getElementById('save-scene').onclick = () => {
const data = JSON.stringify(getSceneData(), null, 2);
const blob = new Blob([data], {type: "application/json"});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `cavebot-scene-${Date.now()}.json`;
a.click();
};
document.getElementById('load-scene').onclick = () => document.getElementById('scene-input').click();
document.getElementById('scene-input').onchange = (e) => {
const file = e.target.files[0];
if(!file) return;
const reader = new FileReader();
reader.onload = (ev) => loadSceneData(JSON.parse(ev.target.result));
reader.readAsText(file);
};
function loadSceneData(data) {
document.getElementById('clear-stage').click(); // Clean stage safely
if(data.bgUrl && data.bgUrl !== 'none') {
dom.stage.style.backgroundImage = `url("${data.bgUrl}")`;
dom.stage.dataset.bgId = data.bgId;
}
if(data.items) {
let maxZ = 100;
data.items.forEach(item => {
const asset = allAssets.find(a => a.id === item.id);
if(asset) {
spawnProp(asset, item);
maxZ = Math.max(maxZ, parseInt(item.z) || 100);
}
});
zIndexCounter = maxZ + 1; // BUG FIX: Keep counter higher than loaded assets
}
}
// --- PNG Export Logic (NEW) ---
document.getElementById('export-img').onclick = () => {
selectItem(null); // Deselect before screenshot to remove bounding boxes
const originalGridState = dom.gridOverlay.style.display;
dom.gridOverlay.style.display = 'none'; // Hide grid for screenshot
html2canvas(dom.stage, {
useCORS: true,
backgroundColor: null // Maintain transparent backgrounds if needed
}).then(canvas => {
const link = document.createElement('a');
link.download = `scene-export-${Date.now()}.png`;
link.href = canvas.toDataURL('image/png');
link.click();
dom.gridOverlay.style.display = originalGridState; // Restore grid
});
};
// --- Clipboard Logic ---
document.getElementById('copy-scene').onclick = () => {
const data = JSON.stringify(getSceneData());
navigator.clipboard.writeText(data).then(() => alert("Synaptic pattern copied to clipboard! ⚡️"));
};
document.getElementById('paste-scene').onclick = async () => {
try {
const text = await navigator.clipboard.readText();
const data = JSON.parse(text);
loadSceneData(data);
} catch(e) {
alert("Could not integrate scene. Invalid pattern data.");
}
};
// Keyboard Integration
document.addEventListener('keydown', (e) => {
// Prevent deleting if typing in search box
if(document.activeElement.tagName === 'INPUT') return;
if(e.key === 'Delete' || e.key === 'Backspace') {
if(selectedEl) { selectedEl.remove(); selectItem(null); }
}
// Arrow Key Nudging
if(selectedEl) {
const step = e.shiftKey ? 15 : 2; // Hold shift for larger jumps
if(e.key === 'ArrowUp') { e.preventDefault(); nudge(0, -step); }
if(e.key === 'ArrowDown') { e.preventDefault(); nudge(0, step); }
if(e.key === 'ArrowLeft') { e.preventDefault(); nudge(-step, 0); }
if(e.key === 'ArrowRight') { e.preventDefault(); nudge(step, 0); }
}
});
init();
</script>
</body>
</html>